<?php declare(strict_types=1);
/**
 * @author      xianganyall <xianganyall@gmail.com>
 * @copyright   2023-2025 owner
 **/

namespace Srv\Libs\Plugins\ECRecover\CryptoCurrency;

use Exception;
use GMP;

class PointMathGMP
{
    /**
     * @param array $pt
     * @param $a
     * @param $p
     * @return array
     * @throws Exception
     * doublePoint
     */
    public static function doublePoint(Array $pt, $a, $p):array
    {
        $gcd        = gmp_strval(gmp_gcd(gmp_mod(gmp_mul(gmp_init(2, 10), $pt['y']), $p),$p));
        if($gcd !== '1'){
            throw new Exception('This library not yet supports point at infinity.');
        }
        $slope      = gmp_mod(gmp_mul(gmp_invert(gmp_mod(gmp_mul(gmp_init(2, 10), $pt['y']), $p), $p), gmp_add(gmp_mul(gmp_init(3, 10), gmp_pow($pt['x'], 2)), $a)), $p);
        $nPt        = [];
        $nPt['x']   = gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt['x']), $pt['x']), $p);
        $nPt['y']   = gmp_mod(gmp_sub(gmp_mul($slope, gmp_sub($pt['x'], $nPt['x'])), $pt['y']), $p);
        return $nPt;
    }

    /**
     * @param array $pt1
     * @param array $pt2
     * @param $a
     * @param $p
     * @return array
     * @throws Exception
     * addPoints
     */
    public static function addPoints(array $pt1, array $pt2, $a, $p):array
    {
        if(gmp_cmp($pt1['x'], $pt2['x']) == 0  && gmp_cmp($pt1['y'], $pt2['y']) == 0){
            return self::doublePoint($pt1, $a, $p);
        }
        $gcd        = gmp_strval(gmp_gcd(gmp_sub($pt1['x'], $pt2['x']), $p));
        if($gcd !== '1'){
            throw new Exception('This library doesn\'t yet supports point at infinity. See https://github.com/BitcoinPHP/BitcoinECDSA.php/issues/9');
        }
        $slope      = gmp_mod(gmp_mul(gmp_sub($pt1['y'], $pt2['y']), gmp_invert(gmp_sub($pt1['x'], $pt2['x']), $p)), $p);
        $nPt        = [];
        $nPt['x']   = gmp_mod(gmp_sub(gmp_sub(gmp_pow($slope, 2), $pt1['x']), $pt2['x']), $p);
        $nPt['y']   = gmp_mod(gmp_sub(gmp_mul($slope, gmp_sub($pt1['x'], $nPt['x'])), $pt1['y']), $p);
        return $nPt;
    }

    /**
     * @param $k
     * @param array $pG
     * @param $a
     * @param $b
     * @param $p
     * @param $base
     * @return array
     * @throws Exception
     * mulPoint
     */
    public static function mulPoint($k, array $pG, $a, $b, $p, $base = null):array
    {
        if($base == 16 || $base == null || is_resource($base)) $k = gmp_init($k, 16);
        if($base == 10) $k = gmp_init($k, 10);
        $kBin       = gmp_strval($k, 2);
        $lastPoint  = $pG;
        for($i = 1; $i < strlen($kBin); $i++){
            if(substr($kBin, $i, 1) === '1'){
                $dPt        = self::doublePoint($lastPoint, $a, $p);
                $lastPoint  = self::addPoints($dPt, $pG, $a, $p);
            }else{
                $lastPoint  = self::doublePoint($lastPoint, $a, $p);
            }
        }
        if(!self::validatePoint(gmp_strval($lastPoint['x'], 16), gmp_strval($lastPoint['y'], 16), $a, $b, $p)){
            throw new Exception('The resulting point is not on the curve.');
        }
        return $lastPoint;
    }

    /**
     * @param $a
     * @param $p
     * @return array|null
     * @throws Exception
     * sqrt
     */
    public static function sqrt($a, $p):?array
    {
        if(gmp_legendre($a, $p) != 1) return null;
        if(gmp_strval(gmp_mod($p, gmp_init(4, 10)), 10) == 3){
            $sqrt1 = gmp_powm($a, gmp_div_q(gmp_add($p, gmp_init(1, 10)), gmp_init(4, 10)), $p);
            $sqrt2 = gmp_mod(gmp_sub($p, $sqrt1), $p);
            return [$sqrt1, $sqrt2];
        }else{
            throw new Exception('P % 4 != 3 , this isn\'t supported yet.');
        }
    }

    /**
     * @param $x
     * @param $a
     * @param $b
     * @param $p
     * @param $derEvenOrOddCode
     * @return array|string|null
     * @throws Exception
     * calculateYWithX
     */
    public static function calculateYWithX($x, $a, $b, $p, $derEvenOrOddCode = null)
    {
        $x  = gmp_init($x, 16);
        $y2 = gmp_mod(gmp_add(gmp_add(gmp_powm($x, gmp_init(3, 10), $p), gmp_mul($a, $x)), $b), $p);
        $y = self::sqrt($y2, $p);
        if(!$y) return null;
        if(!$derEvenOrOddCode){
            return $y;
        }else if($derEvenOrOddCode == '02'){
            $resY = null;
            if(!gmp_strval(gmp_mod($y[0], gmp_init(2, 10)), 10)) $resY = gmp_strval($y[0], 16);
            if(!gmp_strval(gmp_mod($y[1], gmp_init(2, 10)), 10)) $resY = gmp_strval($y[1], 16);
            if($resY){
                while(strlen($resY) < 64){
                    $resY = '0' . $resY;
                }
            }
            return $resY;
        }else if($derEvenOrOddCode == '03'){
            $resY = null;
            if(gmp_strval(gmp_mod($y[0], gmp_init(2, 10)), 10)) $resY = gmp_strval($y[0], 16);
            if(gmp_strval(gmp_mod($y[1], gmp_init(2, 10)), 10)) $resY = gmp_strval($y[1], 16);
            if($resY){
                while(strlen($resY) < 64){
                    $resY = '0'.$resY;
                }
            }
            return $resY;
        }
        return null;
    }

    /**
     * @param $x
     * @param $y
     * @param $a
     * @param $b
     * @param $p
     * @return bool
     * validatePoint
     */
    public static function validatePoint($x, $y, $a, $b, $p):bool
    {
        $x  = gmp_init($x, 16);
        $y2 = gmp_mod(gmp_add(gmp_add(gmp_powm($x, gmp_init(3, 10), $p), gmp_mul($a, $x)), $b), $p);
        $y = gmp_mod(gmp_pow(gmp_init($y, 16), 2), $p);
        return gmp_cmp($y2, $y) === 0;
    }

    /**
     * @param $point
     * @return array
     * negatePoint
     */
    public static function negatePoint($point):array
    {
        return ['x' => $point['x'], 'y' => gmp_neg($point['y'])];
    }

    /**
     * @param $number
     * @return bool
     * isEvenNumber
     */
    public static function isEvenNumber($number):bool
    {
        return (intval($number[strlen($number)-1]) & 1) === 0;
    }

    /**
     * @param $binStr
     * @return GMP
     * bin2gmp
     */
    public static function bin2gmp($binStr):GMP
    {
        $v = gmp_init('0');
        for($i = 0; $i < strlen($binStr); $i++){
            $v = gmp_add(gmp_mul($v, 256), ord($binStr[$i]));
        }
        return $v;
    }
}